Python 的 Class 裡我們透過 __init__
來做初始化一個類別與所需變數,然後透過兩個底線的方法例如 __repr__
, __eq__
來定義資料模型。有沒有更好的方法呢?PEP 557 引入了一個嶄新的定義資料模型方式。結合對變數型態的宣告(PEP 526),雖都知道變數型態的宣告在 Python 是經常被忽視,不過在 PEP 557 倒是有了很大的用處。
首先我們採用文件範例先用過去我們認知的寫法重新表示一次,再來比較新的方式:
class InventoryItem: def __init__(self, name, unit_price, quantity_on_hand=0): self.name = name self.unit_price = unit_price self.quantity_on_hand = quantity_on_hand def __repr__(self): return f'InventoryItem(name={self.name!r} unit_price={self.unit_price!r} quantity_on_hand={self.quantity_on_hand!r})' def total_cost(self): return float(self.unit_price * self.quantity_on_hand) >> item = InventoryItem('hammers', 10.49, 12) >> print(item) >> InventoryItem(name='hammers' unit_price=10.49 quantity_on_hand=12)
很平常的寫法,會生成一個 InvertoryItem 的物件,印出來是一個 __repr__
所返回的代表的描述。物件內有一個 total_cost
方法協助我們計算總花費。 但說實話很繁瑣,為了要清楚定義,必須要使用很多兩個底線的方法來處理 Python 的資料模型。
但現在引入 dataclass 後我們只需要輕輕鬆鬆改寫成:
@dataclass class InventoryItem: name: str unit_price: float quantity_on_hand: int = 0 def total_cost(self) -> float: return self.unit_price * self.quantity_on_hand >> item = InventoryItem('hammers', 10.49, 12) >> print(item) >> InventoryItem(name='hammers', unit_price=10.49, quantity_on_hand=12)
@dataclass
裝飾器會協助增加等價方法的自訂型態到類別內,就不需要在維護這些自訂型態。
def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0) -> None: self.name = name self.unit_price = unit_price self.quantity_on_hand = quantity_on_hand def __repr__(self): return f'InventoryItem(name={self.name!r}, unit_price={self.unit_price!r}, quantity_on_hand={self.quantity_on_hand!r})' def __eq__(self, other): if other.__class__ is self.__class__: return (self.name, self.unit_price, self.quantity_on_hand) == (other.name, other.unit_price, other.quantity_on_hand) return NotImplemented def __ne__(self, other): if other.__class__ is self.__class__: return (self.name, self.unit_price, self.quantity_on_hand) != (other.name, other.unit_price, other.quantity_on_hand) return NotImplemented def __lt__(self, other): if other.__class__ is self.__class__: return (self.name, self.unit_price, self.quantity_on_hand) < (other.name, other.unit_price, other.quantity_on_hand) return NotImplemented def __le__(self, other): if other.__class__ is self.__class__: return (self.name, self.unit_price, self.quantity_on_hand) <= (other.name, other.unit_price, other.quantity_on_hand) return NotImplemented def __gt__(self, other): if other.__class__ is self.__class__: return (self.name, self.unit_price, self.quantity_on_hand) > (other.name, other.unit_price, other.quantity_on_hand) return NotImplemented def __ge__(self, other): if other.__class__ is self.__class__: return (self.name, self.unit_price, self.quantity_on_hand) >= (other.name, other.unit_price, other.quantity_on_hand) return NotImplemented
為何需要額外為了資料模型而增訂這個 PEP? 已經有很多例子可以去做定義:
在 PEP 526 已經有一個簡潔方式去定義 class 的成員,而這個 PEP 在這個基礎之下提供一個簡單且不會太突兀的方式來定義資料模型,除了沒有指定屬性類型的設置。dataclass
是一個真正的類別,所以也不用擔心在繼承過程中影響其他的類別等副作用。
資料模型的建置目地是能有一個靜態的類別型態檢查器,在 PEP 526 的使用就是一例,這裡設計 fields()
函式與 @dataclass
裝飾器, 由於動態的本質,上面所提到的一些 library 會很難與檢查器一起使用。
哪裡不適合呢?
以上,開始擁抱 Python 新的 Data Classes 吧!